/*
 *      Copyright 2012-2013 Battams, Derek
 *       
 *       Licensed under the Apache License, Version 2.0 (the "License");
 *       you may not use this file except in compliance with the License.
 *       You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *       Unless required by applicable law or agreed to in writing, software
 *       distributed under the License is distributed on an "AS IS" BASIS,
 *       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *       See the License for the specific language governing permissions and
 *       limitations under the License.
 */
package org.schedulesdirect.api;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.fluent.Request;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.schedulesdirect.api.exception.InvalidCredentialsException;
import org.schedulesdirect.api.exception.InvalidZipCodeException;
import org.schedulesdirect.api.exception.ServiceOfflineException;
import org.schedulesdirect.grabber.JsonRequest;
import org.schedulesdirect.grabber.JsonRequest.RequestAction;
import org.schedulesdirect.grabber.JsonRequest.RequestObject;
import org.schedulesdirect.grabber.Utils;

/**
 * An implementation of EpgClient that accesses all data from the Schedules Direct JSON feed servers
 * 
 * <p>Use this implementation of EpgClient when you need to download the latest available data directly
 * from the Schedules Direct servers.  Use of this client type will always hit the Schedules Direct
 * servers for all requests.</p>
 * 
 * <p>
 * In almost all conceivable real world application scenarions, the NetworkEpgClient should only
 * be used to perform management functions (i.e. check SD server status, check user account status,
 * change user password, submit metadata updates, etc.)  It is rare that an application would ever
 * use the NetworkEpgClient instance to directly access EPG data.  Instead, applications should
 * always generate and maintain a local cache of their EPG data by downloading it via the sdjson
 * grabber application.  The grabber is extremely efficient at maintaining a local cache of a user's
 * EPG data and only requesting data that has changed and needs to be updated in the local cache.
 * Applicatons should periodically call the grabber to update their local cache then use ZipEpgClient
 * instances to access the EPG data in their Java applications.
 * </p>
 * 
 * <p>
 * The NetworkEpgClient class is capable of accessing all of the EPG objects, but it does so one
 * object at a time and sends a single network request to the Schedules Direct servers for every
 * such object fetch.  Besides being inefficient, this method is extremely slow.  Such access to
 * the EPG data objects is provided for use by the sdjson grabber application and for development,
 * debugging, and testing of the sdjson package.  Most applications should <b>not</b> directly access
 * their EPG data via this class.  Instead, use the grabber app to create a <code>.epg</code> file
 * then feed that file into an instance of ZipEpgClient to access your EPG data from the cache file,
 * which is orders of magnitude faster than using this class.
 * </p>
 * 
 * <p>
 * <b>NOTE:</b> Program objects fetched with a NetworkEpgClient will <b>never</b> contain metadata.
 * Metadata is only made available when accessed via a ZipEpgClient with a zip file source that
 * contains the metadata.
 * </p>
 * @author Derek Battams &lt;derek@battams.ca&gt;
 *
 */
public class NetworkEpgClient extends EpgClient {
	static private final Log LOG = LogFactory.getLog(NetworkEpgClient.class);
	/*
	 * This is all for the client instance cache; just in case people do use this class to access EPG data, there is some effort to cache what we can
	 */
	static private final Map<String, Object> CACHE = Collections.synchronizedMap(new HashMap<String, Object>());
	static private String getCacheKey(Object obj) {
		if(obj instanceof Program)
			return getCacheKeyForProgram(((Program)obj).getId());
		else if(obj instanceof Station)
			return getCacheKeyForStation(((Station)obj).getId());
		else
			return null;
	}
	static private String getCacheKeyForProgram(String progId) { return "__PROG__" + progId; }
	static private String getCacheKeyForStation(String stationId) { return "__STAT__" + stationId; }
	
	private String id;
	private String password;
	private String hash;
	private UserStatus userStatus;
	private String baseUrl;
	private boolean useCache;
	private SystemStatus systemStatus;

	/**
	 * Constructor
	 * @param id The Schedules Direct username to authorize with
	 * @param pwd The Schedules Direct password to authorize with
	 * @throws InvalidCredentialsException Thrown if the given credentials were invalid
	 * @throws IOException Thrown if there is any IO error communicating with the Schedules Direct servers
	 */
	public NetworkEpgClient(String id, String pwd) throws InvalidCredentialsException, IOException, ServiceOfflineException {
		this(id, pwd, null, null, true);
	}

	/**
	 * Constructor
	 * @param id The Schedules Direct username to authorize with
	 * @param pwd The Schedules Direct password to authorize with
	 * @param userAgent The user agent to send on all requests to the SD servers
	 * @throws InvalidCredentialsException Thrown if the given credentials were invalid
	 * @throws IOException Thrown if there is any IO error communicating with the Schedules Direct servers
	 * @throws ServiceOfflineException Thrown if the web service reports itself as offline/unavailable
	 */
	public NetworkEpgClient(String id, String pwd, String userAgent) throws InvalidCredentialsException, IOException, ServiceOfflineException {
		this(id, pwd, userAgent, null, true);
	}
	
	/**
	 * Constructor
	 * @param id The Schedules Direct username to authorize with
	 * @param pwd The Schedules Direct password to authorize with
	 * @param userAgent The user agent to send on all requests to the SD servers
	 * @param baseUrl The base URL to use for all HTTP communication; most should not set this value as it is for testing and development only!
	 * @param useCache Should the client instance maintain a cache of created objects or hit the SD server on every request?  Though memory intensive, use of the cache is greatly encouraged!
	 * @throws InvalidCredentialsException Thrown if the given credentials were invalid
	 * @throws IOException Thrown if there is any IO error communicating with the Schedules Direct servers	 
	 * @throws ServiceOfflineException Thrown if the web service reports itself as offline/unavailable
	 */
	public NetworkEpgClient(String id, String pwd, String userAgent, String baseUrl, boolean useCache) throws InvalidCredentialsException, IOException, ServiceOfflineException {
		super(userAgent);
		if(id == null || id.length() == 0)
			throw new InvalidCredentialsException("Schedules Direct username cannot be empty!");
		if(pwd == null || pwd.length() == 0)
			throw new InvalidCredentialsException("Schedules Direct password cannot be empty!");
		this.id = id;
		password = pwd;
		hash = null;
		userStatus = null;
		this.baseUrl = baseUrl != null ? baseUrl : Config.DEFAULT_BASE_URL;
		this.useCache = useCache;
		authorize();
	}

	/**
	 * Perform user authorization with Schedules Direct
	 * @throws InvalidCredentialsException Thrown if authorization failed
	 * @throws IOException Thrown on any IO error communicating with the Schedules Direct servers
	 * @throws ServiceOfflineException Thrown if the web service reports itself as offline/unavailable
	 */
	protected void authorize() throws InvalidCredentialsException, IOException, ServiceOfflineException {
		JSONObject creds = new JSONObject();
		try {
			creds.put("username", id);
			/*
			 *  Using the deprecated shaHex() b/c some people still use very old
			 *  version of commons-codec; SageTV being an example
			 */			
			creds.put("password", DigestUtils.shaHex(password));
		} catch(JSONException e) {
			throw new RuntimeException(e);
		}
		JSONObject resp = new JsonRequest(hash, getUserAgent(), baseUrl).submitForJson(RequestAction.GET, RequestObject.RANDHASH, creds);
		if(!Utils.isErrorResponse(resp)) {
			try {
				hash = resp.getString("randhash");
			} catch (JSONException e) {
				throw new IOException(e);
			}
		} else if(resp.optInt("code", ApiResponse.NOT_PROVIDED) == ApiResponse.SERVICE_OFFLINE)
			throw new ServiceOfflineException(resp.optString("message"));
		else
			throw new InvalidCredentialsException(resp.optString("message"));
	}

	/**
	 * Grab the status objects for the user and system from Schedules Direct
	 * @throws IOException On any IO error, including an error response from the server
	 */
	protected void initStatusObjects() throws IOException {
		JSONObject resp = new JsonRequest(hash, getUserAgent(), baseUrl).submitForJson(RequestAction.GET, RequestObject.STATUS, null);
		if(!Utils.isErrorResponse(resp)) {
			userStatus = new UserStatus(resp, id, this);
			try {
				systemStatus = new SystemStatus(resp.getJSONArray("systemStatus"));
			} catch(JSONException e) {
				throw new IOException(e);
			}
		} else
			throw new IOException(resp.optString("message"));
	}
	
	@Override
	public UserStatus getUserStatus() throws IOException {
		if(userStatus == null)
			initStatusObjects();
		return userStatus;
	}

	@Override
	public Headend[] getHeadends() throws IOException {
		Headend[] list = new Headend[0];
		JSONObject req = new JSONObject();
		try {
			req.put("country", "ZZ");
			req.put("postalcode", "Subscribed");
		} catch (JSONException e) {
			throw new IOException(e);
		}
		JSONObject resp = new JsonRequest(hash, getUserAgent(), baseUrl).submitForJson(RequestAction.GET, RequestObject.HEADENDS, req);
		if(!Utils.isErrorResponse(resp)) {
			try {
				JSONArray hes = resp.getJSONArray("data");
				list = new Headend[hes.length()];
				for(int i = 0; i < hes.length(); ++i)
					list[i] = new Headend(hes.getJSONObject(i), this);
			} catch(JSONException e) {
				throw new IOException(e);
			}
		}
		return list;
	}

	@Override
	protected Headend[] getHeadendsForZip(String zip, String isoCountry) throws InvalidZipCodeException, IOException {
		Headend[] list = new Headend[0];
		JSONObject req = new JSONObject();
		try {
			req.put("country", isoCountry);
			req.put("postalcode", zip);
		} catch(JSONException e) {
			throw new IOException(e);
		}
		JSONObject resp = new JsonRequest(hash, getUserAgent(), baseUrl).submitForJson(RequestAction.GET, RequestObject.HEADENDS, req);
		if(!Utils.isErrorResponse(resp)) {
			try {
				JSONArray hes = resp.getJSONArray("data");
				list = new Headend[hes.length()];
				for(int i = 0; i < hes.length(); ++i)
					list[i] = new Headend(hes.getJSONObject(i), this);
			} catch(JSONException e) {
				throw new IOException(e);
			}
		}
		return list;
	}
	
	@Override
	public Headend getHeadendById(String id) throws IOException {
		return new Headend(id, this);
	}

	/**
	 * @return The id used to authenticate with Schedules Direct
	 */
	public String getId() {
		return id;
	}

	/**
	 * @return The password used to authenticate with Schedules Direct
	 */
	public String getPassword() {
		return password;
	}

	/**
	 * @return The unique hashkey received from Schedules Direct for this client session
	 */
	public String getHash() {
		return hash;
	}

	@Override
	public void close() throws IOException {
		purgeCache();
	}
	
	@Override
	protected void finalize() throws Throwable {
		close();
	}

	/**
	 * Update the user's password with Schedules Direct.
	 * 
	 * <p>If this method does not throw an exception, the password change is effective immediately.</p>
	 * 
	 * <p>On success, the password is updated in this client instance as well.</p>
	 * @param newPassword The new password for this account
	 * @throws IOException Thrown if the attempt to change the account's password fails.
	 */
	public void changePassword(String newPassword) throws IOException {
		if(newPassword == null || newPassword.length() == 0)
			throw new IOException("Schedules Direct passwords cannot be zero-length!");
		JsonRequest req = new JsonRequest(getHash(), getUserAgent(), getBaseUrl());
		JSONObject input = new JSONObject();
		try {
			/*
			 *  Using the deprecated shaHex() b/c some people still use very old
			 *  version of commons-codec; SageTV being an example
			 */			
			input.put("newPassword", DigestUtils.shaHex(newPassword));
		} catch (JSONException e) {
			throw new IOException(e);
		}
		JSONObject resp = req.submitForJson(RequestAction.UPDATE, RequestObject.PASSWORD, input);
		if(Utils.isErrorResponse(resp))
			throw new IOException(String.format("Update password returned error! [rc=%d; msg=%s]", resp.optInt("code", -1), resp.optString("message")));
		password = newPassword;
	}
	
	/**
	 * Register a new headend in this user's account.
	 * @param headendId The headend id to register with the user's account
	 * @return The number of headend additions remaining for this account; if 0 then no more headends can be added to this account for 24 hours
	 * @throws IOException Thrown if the attempt to add the headend fails
	 */
	public int addHeadendToAccount(String headendId) throws IOException {
		JsonRequest req = new JsonRequest(getHash(), getUserAgent(), getBaseUrl());
		JSONObject resp = req.submitForJson(RequestAction.ADD, RequestObject.HEADENDS, headendId);
		if(Utils.isErrorResponse(resp))
			throw new IOException(String.format("Add headend returned error! [id=%s; rc=%d; msg=%s]", headendId, resp.optInt("code", -1), resp.optString("message")));
		try {
			return resp.getInt("changesRemaining");
		} catch (JSONException e) {
			throw new IOException("Invalid JSON response received!", e);
		}
	}
	
	/**
	 * Remove a headend from the user's account
	 * @param headendId The headend id to remove
	 * @throws IOException Thrown if the attempt to remove the headend fails
	 */
	public void removeHeadendFromAccount(String headendId) throws IOException {
		JsonRequest req = new JsonRequest(getHash(), getUserAgent(), getBaseUrl());
		JSONObject resp = req.submitForJson(RequestAction.DELETE, RequestObject.HEADENDS, headendId);
		if(Utils.isErrorResponse(resp))
			throw new IOException(String.format("Delete headend returned error! [id=%s; rc=%d; msg=%s]", headendId, resp.optInt("code", -1), resp.optString("message")));
	}
		
	@Override
	protected Airing[] fetchSchedule(Station station) throws IOException {
		Airing[] sched = null;
		if(useCache)
			sched = (Airing[])CACHE.get(getCacheKey(station));
		if(sched == null) {
			List<Airing> schedList = new ArrayList<Airing>();
			JSONArray ids = new JSONArray();
			ids.put(station.getId());
			JsonRequest req = new JsonRequest(hash, getUserAgent(), baseUrl);
			JSONObject resp = req.submitForJson(RequestAction.GET, RequestObject.SCHEDULES, ids);
			if(!Utils.isErrorResponse(resp)) {
				String url;
				try {
					url = resp.getString("URL");
				} catch(JSONException e) {
					throw new IOException(e);
				}
				ZipInputStream data = new ZipInputStream(Request.Get(url).execute().returnContent().asStream());
				ZipEntry ze;
				final String EMPTY_ARRAY = new JSONArray().toString();
				while((ze = data.getNextEntry()) != null) {
					String key = ze.getName();
					if("serverID.txt".equals(key)) {
						LOG.debug(String.format("Request '%s' handled by %s", req.getTargetUrl(), IOUtils.toString(data, "UTF-8")));
						continue;
					}
					key = key.substring(0, key.indexOf('.')).substring(6); // Remove prefix: sched_
					if(key.equals(station.getId())) {
						String input = null;
						JSONArray jarr = null;
						try {
							input = IOUtils.toString(data, "UTF-8");
							if(input.length() == 0) {
								input = EMPTY_ARRAY.toString();
								LOG.warn(String.format("%s: Received empty response!", key));
							}
							jarr = new JSONArray(input);
							for(int i = 0; i < jarr.length(); ++i) {
								JSONObject o = jarr.getJSONObject(i);
								Program p = fetchProgram(o.getString("prog_id"));
								schedList.add(new Airing(o, p, station));
							}
							sched = schedList.toArray(new Airing[0]);
							if(useCache)
								CACHE.put(getCacheKey(station), sched);
						} catch (JSONException e) {
							LOG.error(String.format("Invalid JSON in response! [key=%s]", key), e);
						} catch(IOException e) {
							LOG.error("IOError reading zip stream!", e);
						}
					}
				}
				try { data.close(); } catch(IOException e) { LOG.warn("IOError closing zip stream!", e); }			
			} else
				throw new IOException(resp.optString("message"));
		}
		return sched;
	}

	@Override
	protected Program fetchProgram(String progId) throws IOException {
		return fetchPrograms(new String[] { progId }).values().toArray(new Program[1])[0];
	}

	@Override
	protected Map<Station, Airing[]> fetchSchedules(Lineup lineup) throws IOException {
		Collection<String> misses = new ArrayList<String>();
		Map<Station, Airing[]> scheds = new HashMap<Station, Airing[]>();
		if(useCache)
			for(Station s : lineup.getStations()) {
				Airing[] sched = (Airing[])CACHE.get(getCacheKey(s));
				if(sched != null)
					scheds.put(s, sched);
				else
					misses.add(s.getId());
			}
		else
			for(Station s : lineup.getStations())
				misses.add(s.getId());
		if(misses.size() > 0) {
			JSONObject resp = new JsonRequest(hash, getUserAgent(), baseUrl).submitForJson(RequestAction.GET, RequestObject.SCHEDULES, new JSONArray(misses));
			if(!Utils.isErrorResponse(resp)) {
				String url;
				try {
					url = resp.getString("URL");
				} catch(JSONException e) {
					throw new IOException(e);
				}
				ZipInputStream data = new ZipInputStream(Request.Get(url).execute().returnContent().asStream());
				ZipEntry ze;
				final String EMPTY_ARRAY = new JSONArray().toString();
				while((ze = data.getNextEntry()) != null) {
					List<Airing> sched = new ArrayList<Airing>();
					String key = ze.getName();
					key = key.substring(6, key.indexOf('.')); // Remove prefix sched_
					String input = null;
					JSONArray jarr = null;
					try {
						input = IOUtils.toString(data, "UTF-8");
						if(input.length() == 0) {
							input = EMPTY_ARRAY.toString();
							LOG.warn(String.format("%s: Received empty response!", key));
						}
						jarr = new JSONArray(input);
						Station s = lineup.getStation(key);
						for(int i = 0; i < jarr.length(); ++i) {
							JSONObject o = jarr.getJSONObject(i);
							Program p = fetchProgram(o.getString("prog_id"));
							sched.add(new Airing(o, p, s));
						}
						Airing[] schedArray = sched.toArray(new Airing[0]);
						scheds.put(s, schedArray);
						if(useCache)
							CACHE.put(getCacheKey(s), schedArray);
					} catch (JSONException e) {
						LOG.error(String.format("Invalid JSON in response! [key=%s]", key), e);
					} catch(IOException e) {
						LOG.error("IOError reading zip stream!", e);
					}
				}
				try { data.close(); } catch(IOException e) { LOG.warn("IOError closing zip stream!", e); }
			} else
				throw new IOException(resp.optString("message"));
		}
		return scheds;		
	}

	@Override
	protected Map<String, Program> fetchPrograms(String[] progIds) throws IOException {
		Collection<String> misses = new ArrayList<String>();
		Map<String, Program> progs = new HashMap<String, Program>();
		if(useCache)
			for(String progId : progIds) {
				Program p = (Program)CACHE.get(getCacheKeyForProgram(progId));
				if(p != null)
					progs.put(progId, p);
				else
					misses.add(progId);
			}
		else
			misses.addAll(Arrays.asList(progIds));
		if(misses.size() > 0) {
			JSONObject resp = new JsonRequest(hash, getUserAgent(), baseUrl).submitForJson(RequestAction.GET, RequestObject.PROGRAMS, new JSONArray(misses));
			if(!Utils.isErrorResponse(resp)) {
				String url;
				try {
					url = resp.getString("URL");
				} catch (JSONException e) {
					throw new IOException(e);
				}
				ZipInputStream data = new ZipInputStream(Request.Get(url).execute().returnContent().asStream());
				ZipEntry ze = data.getNextEntry();
				while(ze != null) {
					String key = ze.getName();
					if(!key.equals("serverID.txt")) {
						key = key.substring(0, key.indexOf('.'));
						String input = null;
						JSONObject obj = null;
						try {
							input = IOUtils.toString(data, "UTF-8");
							if(input.length() == 0) {
								input = Program.EMPTY_PROGRAM;
								LOG.warn(String.format("%s: Received empty response!", key));
							}
							obj = new JSONObject(input);
						} catch (JSONException e) {
							LOG.error(String.format("Invalid JSONObject in response! [key=%s]", key), e);
							try {
								obj = new JSONObject(Program.EMPTY_PROGRAM);
							} catch(JSONException je) {  // Just won't ever happen
								throw new RuntimeException(je);
							}
						} catch(IOException e) {
							LOG.error("IOError reading zip stream!", e);
							try {
								obj = new JSONObject(Program.EMPTY_PROGRAM);
							} catch(JSONException je) {  // Just won't ever happen
								throw new RuntimeException(je);
							}
						}
						Program p = new Program(obj);
						progs.put(key, p);
						if(useCache)
							CACHE.put(getCacheKeyForProgram(key), p);
					} else
						LOG.debug(String.format("Request handled by '%s'", key));
					ze = data.getNextEntry();
				}
				try { data.close(); } catch(IOException e) { LOG.warn("IOError closing zip stream!", e); }
			} else
				throw new IOException(resp.optString("message"));
		}
		return progs;
	}

	@Override
	protected Lineup[] fetchLineups(Headend headend) throws IOException {
		Collection<Lineup> lineups = new ArrayList<Lineup>();
		JSONArray jarr = new JSONArray();
		jarr.put(headend.getId());
		JSONObject resp = new JsonRequest(hash, getUserAgent(), baseUrl).submitForJson(RequestAction.GET, RequestObject.LINEUPS, jarr);
		if(!Utils.isErrorResponse(resp)) {
			String url;
			try {
				url = resp.getString("URL");
			} catch(JSONException e) {
				throw new IOException(e);
			}
			ZipInputStream data = new ZipInputStream(Request.Get(url).execute().returnContent().asStream());
			ZipEntry ze = data.getNextEntry();
			final String EMPTY_OBJ = new JSONObject().toString();
			while(ze != null) {
				String key = ze.getName();
				if(EpgClient.SERVER_ID_ENTRY.equals(key)) {
					if(LOG.isDebugEnabled())
						LOG.debug("fetchLineups handled by " + IOUtils.toString(data, "UTF-8"));
				} else {
					key = key.substring(0, key.indexOf('.'));
					String input = null;
					JSONObject obj = null;
					try {
						input = IOUtils.toString(data, "UTF-8");
						if(input.length() == 0) {
							input = EMPTY_OBJ;
							LOG.warn(String.format("%s: Received empty response!", key));
						}
						obj = new JSONObject(input);
						JSONArray devs = obj.getJSONArray("deviceTypes");
						JSONArray metas = obj.getJSONArray("metadata");
						JSONArray stations = obj.getJSONArray("stations");
						for(int i = 0; i < devs.length(); ++i) {
							String devId = devs.getString(i);
							JSONObject meta = findMetadataForDevice(metas, devId);
							JSONObject devData = obj.optJSONObject(devId);
							JSONArray stationMap = null;
							if(devData != null)
								stationMap = devData.optJSONArray("map");
							String heName = headend.getName();
							if(heName == null)
								heName = headend.getId();
							String heLocation = headend.getLocation();
							if(heLocation == null)
								heLocation = "OTA";
							lineups.add(new Lineup(headend.getId(), heName, heLocation, meta, stations, stationMap, this));
						}
					} catch (JSONException e) {
						LOG.error(String.format("Invalid JSONObject in response! [key=%s]", key), e);
						try {
							obj = new JSONObject(EMPTY_OBJ);
						} catch(JSONException je) {  // Just won't ever happen
							throw new RuntimeException(je);
						}
					} catch(IOException e) {
						LOG.error("IOError reading zip stream!", e);
						try {
							obj = new JSONObject(EMPTY_OBJ);
						} catch(JSONException je) {  // Just won't ever happen
							throw new RuntimeException(je);
						}
					}
				}
				ze = data.getNextEntry();
			}
			try { data.close(); } catch(IOException e) { LOG.warn("IOError closing zip stream!", e); }			
		} else
			throw new IOException(resp.optString("message"));
		return lineups.toArray(new Lineup[0]);
	}

	/**
	 * Given an array of metadata objects, find the one that represents the given device name
	 * @param metas The array of metadata objects to inspect
	 * @param dev The device name being sought after
	 * @return The metadata object for the given device name or null if it was not found
	 * @throws JSONException Thrown on any JSON error encountered
	 */
	protected JSONObject findMetadataForDevice(JSONArray metas, String dev) throws JSONException {
		JSONObject val = null;
		for(int i = 0; i < metas.length(); ++i) {
			JSONObject o = metas.getJSONObject(i);
			if(dev.equals(o.optString("device"))) {
				val = o;
				break;
			}
		}
		return val;
	}
	
	@Override
	public void purgeCache() {
		CACHE.clear();
	}

	@Override
	public void purgeCache(Object obj) {
		String k = getCacheKey(obj);
		if(k != null) CACHE.remove(k);
	}
	
	/**
	 * @return the baseUrl
	 */
	public String getBaseUrl() {
		return baseUrl;
	}
		
	@Override
	public void deleteMessage(Message msg) throws IOException {
		JsonRequest req = new JsonRequest(getHash(), getUserAgent(), getBaseUrl());
		JSONArray data = new JSONArray();
		data.put(msg.getId());
		JSONObject resp = req.submitForJson(RequestAction.DELETE, RequestObject.MESSAGE, data);
		if(Utils.isErrorResponse(resp))
			throw new IOException(resp.optString("message"));
	}
	
	@Override
	public SystemStatus getSystemStatus() throws IOException {
		if(systemStatus == null)
			initStatusObjects();
		return systemStatus;
	}
	
	@Override
	protected InputStream fetchLogoStream(Station station) throws IOException {
		return station.getLogo().getUrl().openStream();
	}
}